a tool for shared writing and social publishing
1import { subscribeToPublication } from "app/lish/subscribeToPublication";
2import { cookies } from "next/headers";
3import { redirect } from "next/navigation";
4import { NextRequest, NextResponse } from "next/server";
5import { createOauthClient } from "src/atproto-oauth";
6import { setAuthToken } from "src/auth";
7
8import { supabaseServerClient } from "supabase/serverClient";
9import { URLSearchParams } from "url";
10import {
11 ActionAfterSignIn,
12 parseActionFromSearchParam,
13} from "./afterSignInActions";
14import { inngest } from "app/api/inngest/client";
15
16type OauthRequestClientState = {
17 redirect: string | null;
18 action: ActionAfterSignIn | null;
19};
20
21export async function GET(
22 req: NextRequest,
23 props: { params: Promise<{ route: string; handle?: string }> },
24) {
25 const params = await props.params;
26 let client = await createOauthClient();
27 switch (params.route) {
28 case "metadata":
29 return NextResponse.json(client.clientMetadata);
30 case "jwks":
31 return NextResponse.json(client.jwks);
32 case "login": {
33 const searchParams = req.nextUrl.searchParams;
34 const handle = searchParams.get("handle") as string;
35 // Put originating page here!
36 let redirect = searchParams.get("redirect_url");
37 if (redirect) redirect = decodeURIComponent(redirect);
38 let action = parseActionFromSearchParam(searchParams.get("action"));
39 let state: OauthRequestClientState = { redirect, action };
40
41 // Revoke any pending authentication requests if the connection is closed (optional)
42 const ac = new AbortController();
43
44 const url = await client.authorize(handle || "https://bsky.social", {
45 scope:
46 "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview blob:*/*",
47 signal: ac.signal,
48 state: JSON.stringify(state),
49 });
50
51 return NextResponse.redirect(url);
52 }
53 case "callback": {
54 const params = new URLSearchParams(req.url.split("?")[1]);
55
56 let redirectPath = "/";
57 try {
58 const { session, state } = await client.callback(params);
59 let s: OauthRequestClientState = JSON.parse(state || "{}");
60 redirectPath = decodeURIComponent(s.redirect || "/");
61 let { data: identity } = await supabaseServerClient
62 .from("identities")
63 .select()
64 .eq("atp_did", session.did)
65 .single();
66 if (!identity) {
67 let existingIdentity = (await cookies()).get("auth_token");
68 if (existingIdentity) {
69 let data = await supabaseServerClient
70 .from("email_auth_tokens")
71 .select("*, identities(*)")
72 .eq("id", existingIdentity.value)
73 .single();
74 if (data.data?.identity && data.data.confirmed)
75 await supabaseServerClient
76 .from("identities")
77 .update({ atp_did: session.did })
78 .eq("id", data.data.identity);
79
80 return handleAction(s.action, redirectPath);
81 }
82 const { data } = await supabaseServerClient
83 .from("identities")
84 .insert({ atp_did: session.did })
85 .select()
86 .single();
87 identity = data;
88 }
89
90 // Trigger migration if identity needs it
91 const metadata = identity?.metadata as Record<string, unknown> | null;
92 if (metadata?.needsStandardSiteMigration) {
93 if (process.env.NODE_ENV === "production")
94 await inngest.send({
95 name: "user/migrate-to-standard",
96 data: { did: session.did },
97 });
98 }
99
100 let { data: token } = await supabaseServerClient
101 .from("email_auth_tokens")
102 .insert({
103 identity: identity!.id,
104 confirmed: true,
105 confirmation_code: "",
106 })
107 .select()
108 .single();
109 console.log({ token });
110 if (token) await setAuthToken(token.id);
111
112 // Process successful authentication here
113 console.log("authorize() was called with state:", state);
114
115 console.log("User authenticated as:", session.did);
116 return handleAction(s.action, redirectPath);
117 } catch (e) {
118 console.log(e);
119 redirect(redirectPath);
120 }
121 }
122 default:
123 return NextResponse.json({ error: "Invalid route" }, { status: 404 });
124 }
125}
126
127const handleAction = async (
128 action: ActionAfterSignIn | null,
129 redirectPath: string,
130) => {
131 let parsePath = decodeURIComponent(redirectPath);
132 let url;
133 if (parsePath.includes("://")) url = new URL(parsePath);
134 else url = new URL(decodeURIComponent(redirectPath), "https://example.com");
135 if (action?.action === "subscribe") {
136 let result = await subscribeToPublication(action.publication);
137 if (result.success && result.hasFeed === false)
138 url.searchParams.set("showSubscribeSuccess", "true");
139 }
140
141 let path = url.pathname;
142 if (url.search) path += url.search;
143 if (url.hash) path += url.hash;
144 return parsePath.includes("://") ? redirect(url.toString()) : redirect(path);
145};